package com.jidolstar.astro.datetimes
{
	/**
	 * 날짜를 나타낸다. 특정한 지역을 기반으로 하지 않는다.
	 * @author Yongho, Ji
	 * @since 2009.03.27
	 * @see Astronomical Algorithms in Meeus P.59
	 */ 
	public class AstroDate
	{
		private var _jd:Number;
		
		private var _year:int;
		private var _month:int;
		private var _day:int;
		private var _calendarKind:String;
		
		private var isSetYMD:Boolean = false;
		
		/**
		 * 날짜 생성자 
		 * @param jdOrYear JulianDate의 인스턴스 또는 Year 값이다. JulianDate의 인스턴스의 경우에는 month, day가 무시된다.
		 * @param month 월. 1~12내의 값 입력 
		 * @param day 일. 1~31내의 값 입력
		 */ 
		public function AstroDate( jdOrYear:Object, month:int = 1, day:int = 1 )
		{
			if( jdOrYear is JulianDate )
			{
				setJulianDate( jdOrYear as JulianDate );	
			}
			else if ( jdOrYear is Number )
			{
				setYMD( jdOrYear as Number, month, day );
			}
			else
			{
				throw ArgumentError( "jdOrYear argument have to use instance of JulianDate or Number Type Value" ); 
			}
		}
		
		public function get jd():Number
		{
			return _jd;
		}
		
		/**
		 * JulianDate로 날짜를 맞춘다.
		 * Julian Date값을 0.5로 끝나도록 수정된다. 왜냐하면 하루의 시작은 자정이기 때문이다.
		 * @param julainDate JulianDate의 인스턴스 
		 */ 
		public function setJulianDate( julianDate:JulianDate ):void
		{
			_jd = Math.floor( julianDate.jd - 0.5 ) + 0.5;
			isSetYMD = false;
		}
		
		/**
		 * Year, Month, day값으로 날짜를 맞춘다. 
		 * 만약 1582.10.05부터  1582.10.14내의 날짜로 맞추면 ArgumentError가 발생한다. 
		 * 왜냐하면 이때는  Julian Calendar에서 Gregorian Calandar로 변경되어  실제로 없는 날이기 때문이다.
		 * Julian Date값을 0.5로 끝나도록 수정된다. 왜냐하면 하루의 시작은 자정이기 때문이다.
		 * @param year 년도
		 * @param month 월(1~12)
		 * @param day 일(1~31)
		 */ 
		public function setYMD( year:int, month:int, day:int ):void
		{
			var jd:Number = JulianDate.YMDHMS2jd( year, month, day, 0, 0, 0 );
			_jd = Math.floor( jd - 0.5 ) + 0.5;
			isSetYMD = false;
		}
		
		/**
		 * 년도와 그 해의 날수값으로 설정  
		 * @param year 년도 
		 * @param dayNumberInYear year 해의 날 수 
		 * @see Astronomical Algorithms in Meeus P.66
		 */
		public function setYearAndDayNumberInYear( year:int, dayNumberInYear:int ):void
		{
			var K:int = AstroDate.isLeapYear( year ) ? 1 : 2;
			
			var M:int;
			if ( dayNumberInYear < 32 )
			{
				M = 1;
			}
			else
			{
				M = Math.floor( 9 * ( K + dayNumberInYear ) / 275.0 + 0.98 );
			}
			var D:int = dayNumberInYear - Math.floor( 275.0 * M / 9 ) + K * Math.floor( (M + 9) / 12 ) + 30;
			setYMD( year, M, D );
		} 
		
		/**
		 * year, month, day를 계산한다.  
		 * @see Astronomical Algorithms in Meeus P.59
		 */ 
		private function calculateYMD():void
		{
			if( !isSetYMD )
			{
				var Z:Number = _jd + 0.5; 
				var A:Number;
				if( Z < 2299161 )
				{
					A = Z;
					_calendarKind = CalendarType.JULIAN;
				}
				else
				{
					var a:Number = Math.floor( (Z - 1867216.25) / 36524.25);
					A = Z + 1 + a - Math.floor( a / 4.0 );
					_calendarKind = CalendarType.GREGORIAN;
				}
				var B:Number = A  + 1524;
				var C:Number = Math.floor( (B - 122.1) / 365.25);
				var D:Number = Math.floor( 365.25 * C);
				var E:Number = Math.floor( (B - D) / 30.6001);		

				_day = B - D - Math.floor( 30.6001 * E ); //계산의 정확성을 위해 30.6001 대신 30.6을 넣지 말자. 
				_month = ( E < 14 )? E-1 : E-13;
				_year = ( _month > 2 ) ? Math.floor(C - 4716): Math.floor(C - 4715);
				
				isSetYMD = true;
			}
		}
		
		/**
		 * 년도
		 */ 
		public function get year():int
		{
			if( !isSetYMD ) calculateYMD();
			return _year;
		}
		
		/**
		 * 월
		 */ 
		public function get month():int
		{
			if( !isSetYMD ) calculateYMD();
			return _month;
		}
		
		/**
		 * 일
		 */ 
		public function get day():int
		{
			if( !isSetYMD ) calculateYMD();
			return _day;
		}
		
		/**
		 * 현재 설정된 달력의 종류 
		 * 설정된 년월일 또는 JulianDate에 따라서 
		 */ 
		public function get currentCalendarKind():String
		{
			if( !isSetYMD ) calculateYMD();
			return _calendarKind;
		}

		/**
		 * 윤년인가?
		 */ 
		public function get isLeapYear():Boolean
		{
			if( year % 4 == 0 )
			{
				if( year % 100 == 0 )
				{
					if( currentCalendarKind == CalendarType.GREGORIAN )
					{
						if( year % 400 == 0 )
							return true;
						return false;
					}
					else
					{
						return false;
					}
				}
				return true;
			}
			return false;
		}
		
		/**
		 * 해당 년도에서  현재 날짜까지의 날수 
		 * 
		 * @see Astronomical Algorithms in Meeus, P65
		 */ 
		public function get dayNumberOfTheYear():int
		{
			var K:int = isLeapYear ? 1 : 2;
			var N:int = Math.floor( 275 * month / 9 ) - K * Math.floor( ( month + 9 ) / 12 ) + day - 30; 
			return N;
		}
		
		/**
		 * 해당 달의 날 수 
		 */ 
		public function get dayNumberOfTheMonth():int
		{
			switch(month)
			{
				case 2: 
					return isLeapYear ? 29 : 28;
				case 4: 
				case 6: 
				case 9: 
				case 11:
					return 30;
				default:
					return 31;
			}			
		}
		
		/**
		 * 해당 주의 요일 
		 * <p>
		 * 0:Sunday, 1:Monday, 2:Thuseday, 3:Wednesday, 4:Thursday, 5:Friday, 6:Saturday
		 * </p>
		 * @see Astronomical Algorithms in Meeus, P65
		 */ 
		public function get dayOfTheWeek():int
		{
			return Math.floor(_jd + 1.5) % 7;
		}
		
		/**
		 * 클론생성 반환 
		 */ 
		public function clone():AstroDate
		{
			return new AstroDate( this.year, this.month, this.day );
		}
		
		/**
		 * 날짜가 같은지 확인 
		 */ 
		public function equal( date:AstroDate ):Boolean
		{
			if( date == null ) return false;
			if( this._jd == date._jd ) return true;
			return false;
		}
		
		/**
		 * 해당 년도가 윤년인가?
		 */ 
		public static function isLeapYear( year:int ):Boolean
		{
			if( year % 4 == 0 )
			{
				if( year % 100 == 0 )
				{
					if( year > 1582 )
					{
						if( year % 400 == 0 )
							return true;
						return false;
					}
					else
					{
						return false;
					}
				}
				return true;
			}
			return false;			
		}
		
		/**
		 * 해당 년도에서 현재 날짜까지의 날수 
		 * @param year
		 * @param month
		 * @param day
		 * @return day of the year
		 * @see Astronomical Algorithms in Meeus, P65		 
		 */ 
		public static function getDayNumberOfTheYear( year:int, month:int, day:int ):Number
		{
			var K:int = AstroDate.isLeapYear( year ) ? 1 : 2;
			var N:int = Math.floor( 275 * month / 9 ) - K * Math.floor( ( month + 9 ) / 12 ) + day - 30; 
			return N;			
		}
		
		/**
		 * 해당 년도,월의 날수 
		 * @param year
		 * @param month
		 * @return day of the month
		 */ 
		public static function getDayNumberOfTheMonth( year:int, month:int ):int
		{
			switch(month)
			{
				case 2: 
					return AstroDate.isLeapYear( year ) ? 29 : 28;
				case 4: 
				case 6: 
				case 9: 
				case 11:
					return 30;
				default:
					return 31;
			}			
		}
		
		/**
		 * 요일 (Julian Date 값으로 계산)
		 * <p>
		 * 0:Sunday, 1:Monday, 2:Thuseday, 3:Wednesday, 4:Thursday, 5:Friday, 6:Saturday
		 * </p>
		 * @param Julian Date 
 		 * @return day of the week
 		 * @see Astronomical Algorithms in Meeus, P65
		 */ 
		public static function getDayOfTheWeekFromJD( jd:Number ):int
		{
			return Math.floor(jd + 1.5) % 7;
		}		
		
		/**
		 * 요일 ( year, month, day 값으로 계산)
		 * @param year
		 * @param month
		 * @param day
		 * @return day of the week
		 * <p>
		 * 0:Sunday, 1:Monday, 2:Thuseday, 3:Wednesday, 4:Thursday, 5:Friday, 6:Saturday
		 * </p>
		 */ 
		public static function getDayOfTheWeekFromYMD( year:int, month:int, day:int ):int
		{
			var jd:Number = JulianDate.YMDHMS2jd( year, month, day );
			return AstroDate.getDayOfTheWeekFromJD( jd );
		}
				
	}
}